
package com.binance.gateway.ribbon.retry;

import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.IClientConfig;
import com.netflix.client.config.IClientConfigKey;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerStats;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient.RibbonServer;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerContext;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;

import java.net.ConnectException;
import java.util.ArrayList;
import java.util.List;

@Slf4j
public class BinanceRibbonLoadBalancedRetryPolicy implements LoadBalancedRetryPolicy {

	public static final IClientConfigKey<String> RETRYABLE_STATUS_CODES = new CommonClientConfigKey<String>("retryableStatusCodes") {};
	private int sameServerCount = 0;
	private int nextServerCount = 0;
	private String serviceId;
	private RibbonLoadBalancerContext lbContext;
	private ServiceInstanceChooser loadBalanceChooser;
	List<Integer> retryableStatusCodes = new ArrayList<>();
	
	public BinanceRibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser) {
		this.serviceId = serviceId;
		this.lbContext = context;
		this.loadBalanceChooser = loadBalanceChooser;
	}

	public BinanceRibbonLoadBalancedRetryPolicy(String serviceId, RibbonLoadBalancerContext context, ServiceInstanceChooser loadBalanceChooser,
												IClientConfig clientConfig) {
		this.serviceId = serviceId;
		this.lbContext = context;
		this.loadBalanceChooser = loadBalanceChooser;
		String retryableStatusCodesProp = clientConfig.getPropertyAsString(RETRYABLE_STATUS_CODES, "");
		String[] retryableStatusCodesArray = retryableStatusCodesProp.split(",");
		for(String code : retryableStatusCodesArray) {
			if(!StringUtils.isEmpty(code)) {
				try {
					retryableStatusCodes.add(Integer.valueOf(code.trim()));
				} catch (NumberFormatException e) {
					log.error("BinanceRibbonLoadBalancedRetryPolicy error",e);
				}
			}
		}
	}

	private boolean canRetry(LoadBalancedRetryContext context) {
		HttpMethod method = context.getRequest().getMethod();

		// 对GET请求，都进行重置
		if (HttpMethod.GET == method) {
			return true;
		}

		// 对非GET请求，只对ConnectException重试
		if (HttpMethod.GET != method && (context.getLastThrowable() instanceof ConnectException)) {
			return true;
		}

		return false;
	}

	@Override
	public boolean canRetrySameServer(LoadBalancedRetryContext context) {
		HttpMethod method = context.getRequest().getMethod();

		boolean canRetrySameServer;

		// ConnectException 不RetrySameServer，直接RetryNextServer
		if (null != context.getLastThrowable() && (context.getLastThrowable() instanceof ConnectException)) {
			canRetrySameServer = false;
		} else {
			canRetrySameServer = sameServerCount < lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context);
		}

		log.info("feignRibbon canRetrySameServer={}, method:{}, sameServerCount:{}, exception:{}", canRetrySameServer, method, sameServerCount, context.getLastThrowable());
		return canRetrySameServer;
	}

	@Override
	public boolean canRetryNextServer(LoadBalancedRetryContext context) {
		HttpMethod method = context.getRequest().getMethod();
		boolean canRetryNextServer = nextServerCount <= lbContext.getRetryHandler().getMaxRetriesOnNextServer() && canRetry(context);
		log.info("feignRibbon canRetryNextServer={}, method:{}, nextServerCount:{}, exception:{}", canRetryNextServer, method, nextServerCount, context.getLastThrowable());
		return canRetryNextServer;
	}

	@Override
	public void close(LoadBalancedRetryContext context) {

	}

	@Override
	public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) {
		//if this is a circuit tripping exception then notify the load balancer
		if (lbContext.getRetryHandler().isCircuitTrippingException(throwable)) {
			updateServerInstanceStats(context);
		}
		
		//Check if we need to ask the load balancer for a new server.
		//Do this before we increment the counters because the first call to this method
		//is not a retry it is just an initial failure.
		if(!canRetrySameServer(context)  && canRetryNextServer(context)) {
			context.setServiceInstance(loadBalanceChooser.choose(serviceId));
		}
		//This method is called regardless of whether we are retrying or making the first request.
		//Since we do not count the initial request in the retry count we don't reset the counter
		//until we actually equal the same server count limit.  This will allow us to make the initial
		//request plus the right number of retries.
		if(sameServerCount >= lbContext.getRetryHandler().getMaxRetriesOnSameServer() && canRetry(context)) {
			//reset same server since we are moving to a new server
			sameServerCount = 0;
			nextServerCount++;
			if(!canRetryNextServer(context)) {
				context.setExhaustedOnly();
			}
		} else {
			sameServerCount++;
		}

	}
	
	private void updateServerInstanceStats(LoadBalancedRetryContext context) {
		ServiceInstance serviceInstance = context.getServiceInstance();
		if (serviceInstance instanceof RibbonServer) {
			Server lbServer = ((RibbonServer)serviceInstance).getServer();
			ServerStats serverStats = lbContext.getServerStats(lbServer);
			serverStats.incrementSuccessiveConnectionFailureCount();
			serverStats.addToFailureCount();    				
			log.debug(lbServer.getHostPort() + " RetryCount: " + context.getRetryCount()
				+ " Successive Failures: " + serverStats.getSuccessiveConnectionFailureCount() 
				+ " CircuitBreakerTripped:" + serverStats.isCircuitBreakerTripped());
		}
	}

	@Override
	public boolean retryableStatusCode(int statusCode) {
		return retryableStatusCodes.contains(statusCode);
	}
}
